﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using TwainLib;
using System.Windows.Forms;
using System.IO;
using System.Runtime.InteropServices;
using System.Collections;

namespace ImageAcquisitionTAL
{
    public class TWAINTechnology : IAcquisitionTechnology
    {
        internal TwainSession CurrentApp = null;

        #region IAcquisitionTechnology Members

        internal class CommandHandlerForm : Form, IMessageFilter
        {
            internal bool ImageReady = false;
            internal bool Signal = false;
            private TwIdentity currentAppId = null;
            private TwIdentity currentDS = null;

            #region IMessageFilter Members

            public bool PreFilterMessage(ref Message m)
            {
                if (currentAppId == null || currentDS == null)
                    return false;
                TwainCommand cmd = Twain.PassMessage(ref m, currentAppId, currentDS);
                if (cmd == TwainCommand.Not)
                    return false;
                switch (cmd)
                {
                    case TwainCommand.CloseRequest: 
                    case TwainCommand.CloseOk :
                        Signal = true;
                        break;
                    case TwainCommand.DeviceEvent:
                        break;
                    case TwainCommand.TransferReady:
                        Signal = true;
                        ImageReady = true;
                        break;
                }
                return true;
            }

            internal void EndListen()
            {
                Application.RemoveMessageFilter(this);
                currentDS = null;
            }

            internal void BeginListen(TwIdentity datasource)
            {
                this.currentDS = datasource;
                Application.AddMessageFilter(this);
            }

            #endregion
            
            public CommandHandlerForm(TwIdentity currentAppId)
            {
                this.currentAppId = currentAppId;
            }
        }

        internal class TwainSession
        {
            internal TwIdentity AppId;
            internal CommandHandlerForm Handler;
            internal TwIdentity CurrentDS;
        }

        private Dictionary<string, TwIdentity> installedDevices = new Dictionary<string, TwIdentity>();
        
        private string GetDeviceId(TwIdentity dataSource)
        {
            return string.Format("Manufacturer={0}||ProductFamily={1}||ProductName={2}",
                dataSource.Manufacturer,
                dataSource.ProductFamily,
                dataSource.ProductName);
        }

        public IEnumerable<ScannerInfo> GetInstalledScanners()
        {
            List<ScannerInfo> DSs = new List<ScannerInfo>();
            OpenDSM();
            if (CurrentApp == null)
                return new List<ScannerInfo>();
            try
            {
                TwIdentity ds = new TwIdentity();
                TwRC rc = Twain.DSMident(CurrentApp.AppId, IntPtr.Zero, TwDG.Control, TwDAT.Identity, TwMSG.GetFirst, ds);
                while (rc == TwRC.Success)
                {
                    ScannerInfo s = new ScannerInfo();
                    s.DeviceID = GetDeviceId(ds);
                    s.DisplayName = GetDeviceName(ds);
                    s.Technology = this;
                    installedDevices.Add(s.DeviceID, ds);
                    DSs.Add(s);
                    ds = new TwIdentity();
                    rc = Twain.DSMident(CurrentApp.AppId, IntPtr.Zero, TwDG.Control, TwDAT.Identity, TwMSG.GetNext, ds);
                }
            }
            finally
            {
                CloseDSM();
            }
            return DSs;
        }

        private void CloseDSM()
        {
            if (CurrentApp == null)
                return;
            IntPtr h = CurrentApp.Handler.Handle;
            TwRC rc = Twain.DSMparent(CurrentApp.AppId, IntPtr.Zero, TwDG.Control, TwDAT.Parent, TwMSG.CloseDSM, ref h);
            CurrentApp.Handler.Dispose();
            CurrentApp = null;
        }

        private void OpenDSM()
        {
            if (CurrentApp != null)
                return;
            CurrentApp = new TwainSession();            
            CurrentApp.AppId = new TwIdentity();
            Twain.FillIdentity(CurrentApp.AppId);
            CurrentApp.Handler = new CommandHandlerForm(CurrentApp.AppId);
            IntPtr h = CurrentApp.Handler.Handle;
            TwRC rc = TwRC.Failure;
            try
            {
                rc = Twain.DSMparent(CurrentApp.AppId, IntPtr.Zero, TwDG.Control, TwDAT.Parent, TwMSG.OpenDSM, ref h);
            }
            catch (Exception)
            {
                
            }
            if (rc != TwRC.Success)
            {
                CurrentApp.Handler.Dispose();
                CurrentApp = null;
            }
        }

        private bool OpenDS()
        {
            TwRC rc = Twain.DSMident(CurrentApp.AppId, IntPtr.Zero, TwDG.Control, TwDAT.Identity, TwMSG.OpenDS, CurrentApp.CurrentDS);
            if (rc != TwRC.Success)
                CurrentApp.CurrentDS = null;
            return rc == TwRC.Success;
        }

        private TwIdentity GetDataSource(string id)
        {
            if (string.IsNullOrEmpty(id))
                return null;
            if (installedDevices.ContainsKey(id))
                return installedDevices[id];
            else
                return null;
        }

        public ScannerInfo GetScannerInfo(string deviceId)
        {
            OpenDSM();
            try
            {
                if (CurrentApp == null)
                    return null;                
                try
                {
                    CurrentApp.CurrentDS = GetDataSource(deviceId);
                    if (!OpenDS())
                        return null;
                    ScannerInfo s = new ScannerInfo();
                    s.DeviceID = deviceId;
                    s.DisplayName = GetDeviceName(CurrentApp.CurrentDS);
                    s.Technology = this;
                    s.SupportedResolutions = GetSupportedResolutions();
                    s.SupportsFeeder = GetSupportsFeeder(CurrentApp.AppId, CurrentApp.CurrentDS);
                    GetSupportedColorModes(CurrentApp.AppId, CurrentApp.CurrentDS, ref s.SupportsBW, ref s.SupportsGrayScale, ref s.SupportsColors);
                    return s;
                }
                finally
                {
                    CloseDS();
                }
            }
            finally
            {
                CloseDSM();
            }
        }

        private void CloseDS()
        {
            if (CurrentApp == null)
                return;
            if (CurrentApp.CurrentDS == null)
                return;
            TwRC rc = Twain.DSMident(CurrentApp.AppId, IntPtr.Zero, TwDG.Control, TwDAT.Identity, TwMSG.CloseDS, CurrentApp.CurrentDS);
            CurrentApp.CurrentDS = null;
        }

        private object GetOneValueCapa(Type type, TwCap capaId)
        {
            TwCapability capa = new TwCapability(capaId);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Get, capa),
                "Cannot get capability {0}", capaId);
            IntPtr container = Twain.GlobalLock(capa.Handle);
            try
            {
                return GetContainerContents(type, container);
            }
            finally
            {
                Twain.GlobalUnlock(container);
            }
        }

        private bool AssertOK(TwRC rc, TwCC[] expectedCCs, string text, params object[] moreInfo)
        {
            if (rc != TwRC.Success && rc != TwRC.XferDone && rc != TwRC.Cancel)
            {
                TwCC cc = GetDSStatus();
                if (expectedCCs.Any(cct => cct == cc))
                    return true;
                Throw(text, cc, moreInfo);
            }
            return true;
        }

        private void AssertOK(TwRC rc, string text, params object[] moreInfo)
        {
            if (rc != TwRC.Success && rc != TwRC.XferDone && rc != TwRC.Cancel)
            {
                TwCC cc = GetDSStatus();
                Throw(text, cc, moreInfo);
            }
        }

        private static void Throw(string text, TwCC cc, params object[] moreInfo)
        {
            throw new Exception(string.Format("TWAIN operation failed: {0}. Status: {1}", string.Format(text, moreInfo), cc));
        }

        private void GetSupportedColorModes(TwIdentity appId, TwIdentity dataSource, 
            ref bool supportsBW, ref bool supportsGrayScale, ref bool supportsColors)
        {
            TwCapability capa = new TwCapability(TwCap.IPixelType);
            TwRC rc = Twain.DScap(appId, dataSource, TwDG.Control, TwDAT.Capability, TwMSG.Get, capa);
            IntPtr container = Twain.GlobalLock(capa.Handle);
            try
            {
                supportsBW = false;
                supportsColors = false;
                supportsGrayScale = false;
                switch ((TwOn)capa.ConType)
                {
                    case TwOn.Enum:
                        object[] values = GetContainerContents(typeof(short), container, (TwOn)capa.ConType);
                        foreach (short mode in values)
                            ConvertMode(ref supportsBW, ref supportsGrayScale, ref supportsColors, mode);
                        break;
                    case TwOn.One:
                        short md = (short)GetContainerContents(typeof(short), container);
                        ConvertMode(ref supportsBW, ref supportsGrayScale, ref supportsColors, md);
                        break;
                    default:                        
                        throw new Exception("Container type is not TW_ENUMERATION, TW_ONEVALUE or TW_RANGE");
                }
            }
            finally
            {
                Twain.GlobalUnlock(container);
            }
            AssertOK(rc, "Cannot check color capabilities of scanner");
        }

        private static void ConvertMode(ref bool supportsBW, ref bool supportsGrayScale, ref bool supportsColors, short mode)
        {
            if (mode == (short)TwPt.TWPT_BW)
                supportsBW = true;
            if (mode == (short)TwPt.TWPT_GRAY)
                supportsGrayScale = true;
            if (mode == (short)TwPt.TWPT_RGB)
                supportsColors = true;
        }

        private bool GetSupportsFeeder(TwIdentity app, TwIdentity dataSource)
        {            
            TwCapability capa = new TwCapability(TwCap.FeederEnabled);
            TwRC rc = Twain.DScap(app, dataSource, TwDG.Control, TwDAT.Capability, TwMSG.Get, capa);
            return rc == TwRC.Success;
        }

        private int[] GetSupportedResolutions()
        {
            // we're assume that units is DPI and XRes always = YRes ("square" pixels)
            TwCapability capa = new TwCapability(TwCap.IXResolution);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Get, capa),
                "Cannot get supported resolutions");
            IntPtr container = Twain.GlobalLock(capa.Handle);
            try
            {
                switch ((TwOn)capa.ConType)
                {
                    case TwOn.Enum:
                    case TwOn.Range:
                        object[] values = GetContainerContents(typeof(TwFix32), container, (TwOn)capa.ConType);
                        int[] result = new int[values.Length];
                        for (int i = 0; i < result.Length; i++)
                            result[i] = ((TwFix32)values[i]).Whole;
                        return result;
                    case TwOn.One:
                        return new int[] {  ((TwFix32)GetContainerContents(typeof(TwFix32), container)).Whole };
                    default:
                        throw new Exception("Container type is not TW_ENUMERATION, TW_ONEVALUE or TW_RANGE");
                }
            }
            finally
            {
                Twain.GlobalUnlock(container);
            }
        }

        /// <summary>
        /// // for one value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="container"></param>
        /// <returns></returns>
        private object GetContainerContents(Type type, IntPtr container)
        {
            return Read(type, container, 2);
        }

        /// <summary>
        /// // for multiple values
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="container"></param>
        /// <param name="containerType"></param>
        /// <returns></returns>
        private object[] GetContainerContents(Type type, IntPtr container, TwOn containerType)
        {
            List<object> contents = new List<object>();
            if (containerType == TwOn.Enum)
            {
                int count = Marshal.ReadInt32(container, 2);
                for (int pos = 14 /* start of ItemList*/;
                    pos < 14 + count * Marshal.SizeOf(type);
                    pos += Marshal.SizeOf(type))
                {
                    contents.Add(Read(type, container, pos));
                }
            }
            else if (containerType == TwOn.Range)
            {
                object min = Read(type, container, 2);
                object max = Read(type, container, 6);
                object step = Read(type, container, 10);
                for (object v = min; VLessOrEqualMax(type, v, max); v = IncVStep(type, v, step))
                    contents.Add(v);
            }
            else
            {
                throw new Exception("Use another overload method with this type of container");
            }
            return contents.ToArray();
        }

        private object IncVStep(Type type, object v, object step)
        {
            if (v is bool || v is byte || v is short || v is int)
                return ((int)v + (int)step); // Limitation! Only whole part is used
            else if (v is TwFix32)
            {
                TwFix32 fx = new TwFix32();
                fx.Whole = (short)(((TwFix32)v).Whole + ((TwFix32)step).Whole);
                return fx;
            }
            else
                throw new NotImplementedException("Internal error: only bool, byte, short, int or TW_FIX32 implemented");
        }

        private bool VLessOrEqualMax(Type type, object v, object max)
        {
            if (v is TwFix32)
            {
                return (((TwFix32)v).Whole <= ((TwFix32)max).Whole);
            }
            else if (v is bool || v is byte || v is short || v is int)
            {
                return (int)v <= (int)max;
            }
            else
                throw new NotImplementedException("Internal error: only bool, byte, short, int or TW_FIX32 implemented");
        }

        private object Read(Type type, IntPtr container, int position)
        {
            object val = Activator.CreateInstance(type);
            if (val is TwFix32)
            {
                TwFix32 fx = (TwFix32)val;
                fx.Whole = Marshal.ReadInt16(container, position);
                val = fx;
                if ((ushort)Marshal.ReadInt16(container, position + sizeof(short)) != 0)
                    throw new NotImplementedException("Fractional resolutions not implemented, though there is something in fractional part");
            }
            else if (val is byte)
                val = (byte)Marshal.ReadByte(container, position);
            else if (val is short)
                val = (short)(object)Marshal.ReadInt16(container, position);
            else if (val is int)
                val = (int)(object)Marshal.ReadInt32(container, position);
            else if (val is bool)
            {
                short v = (short)(object)Marshal.ReadInt16(container, position);
                val = v == 0 ? false : true;
            }
            else
                throw new NotImplementedException("Only reading of bool, byte, short, int or TW_FIX32 implemented");
            return val;
        }

        private string GetDeviceName(TwIdentity dataSource)
        {
            return string.Format("{0}: {1} ({2})",
                dataSource.Manufacturer,
                dataSource.ProductName,
                dataSource.ProductFamily);
        }

        public bool IsAvailable()
        {
            return File.Exists(Environment.SystemDirectory + "\\..\\twain_32.dll");
        }

        private TwPt GetPixelTypeByColorMode(ColorMode mode)
        {
            switch (mode)
            {
                case ColorMode.BW :
                    return TwPt.TWPT_BW;
                case ColorMode.Color :
                    return TwPt.TWPT_RGB;
                case ColorMode.GrayScale :
                    return TwPt.TWPT_GRAY;
                default :
                    throw new Exception("Internal error: unexpected coloe mode 'None'");
            }
        }

        public Image Acquire(ScannerInfo scanner)
        {
            if (session == null)
                throw new Exception("Scanning session must be started with BeginAcquire()");
            try
            {
                // check if we use ADF no more pages in feeder to scan, then return null
                if (scanner.CurrentSource == ScanSource.Feeder)
                    if (!(bool)GetOneValueCapa(typeof(bool), TwCap.FeederLoaded))
                        return null;

                // wait for events from handler
                while (!CurrentApp.Handler.Signal)
                    Application.DoEvents();

                if (CurrentApp.Handler.ImageReady)
                {
                    GetImageInfo();
                    // get image and create Image object via reading STREAM, NOT FILE!!! and return ready Image object
                    Image img = GetImage();
                    AcknowledgeTransfer();
                    return img;
                }
                else
                    return null;                
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Error while acquiring image from scanner. Error occured: {0}", ex.Message));
            }
        }

        private void AcknowledgeTransfer()
        {
            TwPendingXfers pxfr = new TwPendingXfers();
            AssertOK(Twain.DSpxfer(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.PendingXfers, TwMSG.EndXfer, pxfr),
                "Cannot acknowledge transfer");
            session.moreImagesToScan = pxfr.Count != 0;
        }

        private void SetupScanner(ScannerInfo scanner)
        {
            // if we work with feeder?
            if (scanner.CurrentSource == ScanSource.Feeder)
            {
                // switch ADF on
                SwitchFeederOn();
                SwitchAutoFeedOff();
            }
            else
            {
                // switch ADF off
                SwitchFeederOff();
            }

            // set current resolution (scanner.CurrentResolution)
            SetCurrentResolution(scanner);

            // set current color mode (scanner.CurrentColorMode)
            SetCurrentColorMode(scanner);

            // we're gonna scan 1 image
            PrepareToScan(-1);
        }

        private Image GetImage()
        {
            IntPtr hDIB = IntPtr.Zero;
            AssertOK(Twain.DSixfer(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Image, TwDAT.ImageNativeXfer, TwMSG.Get, ref hDIB),
                "Cannot transfer image");
            // process image                                    
            Image img = Twain.CreateBitmapFromDib1(hDIB);
            if (hDIB != IntPtr.Zero)
                Twain.GlobalFree(hDIB);
            return img;
        }

        private void GetImageInfo()
        {
            TwImageInfo iinf = new TwImageInfo();
            AssertOK(Twain.DSiinf(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Image, TwDAT.ImageInfo, TwMSG.Get, iinf),
                "Cannot get information about image");
        }

        private TwUserInterface EnableDataSource()
        {
            CurrentApp.Handler.BeginListen(CurrentApp.CurrentDS);
            TwUserInterface ui = new TwUserInterface();
            ui.ModalUI = 0;
            ui.ShowUI = 0;
            AssertOK(Twain.DSuserif(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.UserInterface, TwMSG.EnableDS, ui),
                "Cannot enable datasource");
            return ui;
        }

        private void PrepareToScan(short images)
        {
            TwCapability xfccapa = new TwCapability(TwCap.XferCount, images);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Set, xfccapa),
                "Cannot set for transfer");
        }

        private void SetCurrentColorMode(ScannerInfo scanner)
        {
            TwPt col = GetPixelTypeByColorMode(scanner.CurrentColorMode);
            TwCapability colcapa = new TwCapability(TwCap.IPixelType, (short)col);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Set, colcapa),
                new TwCC[] { TwCC.BadValue }, "Cannot set color mode to {0}", scanner.CurrentColorMode);
        }

        private void SetCurrentResolution(ScannerInfo scanner)
        {
            TwFix32 xres = new TwFix32();
            xres.Whole = (short)scanner.CurrentResolution;
            TwCapability xrescapa = new TwCapability(TwCap.IXResolution, xres);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Set, xrescapa),
                new TwCC[] { TwCC.BadValue }, "Cannot set resolution to {0}", scanner.CurrentResolution);

            TwFix32 yres = new TwFix32();
            yres.Whole = (short)scanner.CurrentResolution;
            TwCapability yrescapa = new TwCapability(TwCap.IYResolution, yres);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Set, yrescapa),
                new TwCC[] { TwCC.BadValue }, "Cannot set resolution to {0}", scanner.CurrentResolution);
        }

        private void SwitchFeederOff()
        {
            TwCapability adfcapa = new TwCapability(TwCap.FeederEnabled, 0);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Set, adfcapa),
                new TwCC[] { TwCC.BadCap, TwCC.CapUnsupported, TwCC.CapBadOperation, TwCC.BadValue }, "Cannot turn feeder off");
        }

        private void SwitchAutoFeedOff()
        {
            TwCapability autocapa = new TwCapability(TwCap.Autofeed, 0);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Set, autocapa),
                new TwCC[] { TwCC.CapUnsupported, TwCC.CapBadOperation, TwCC.BadValue }, "Cannot turn autofeed off");
        }

        private void SwitchFeederOn()
        {
            TwCapability adfcapa = new TwCapability(TwCap.FeederEnabled, 1);
            AssertOK(Twain.DScap(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Capability, TwMSG.Set, adfcapa),
                new TwCC[] { TwCC.CapUnsupported, TwCC.CapBadOperation, TwCC.BadValue }, "Cannot turn feeder on");
        }

        private TwCC GetDSStatus()
        {
            TwStatus status = new TwStatus();
            TwRC rc = Twain.DSstatus(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.Status, TwMSG.Get, status);
            if (rc != TwRC.Success)
                Throw("Cannot get datasource status", TwCC.Success);
            return (TwCC)status.ConditionCode;
        }

        public Image AcquireForPreview(ScannerInfo scanner)
        {
            ScannerInfo minimal = scanner.Clone() as ScannerInfo;
            if (scanner.SupportsBW)
                minimal.CurrentColorMode = ColorMode.BW;
            else if (scanner.SupportsGrayScale)
                minimal.CurrentColorMode = ColorMode.GrayScale;
            else
                minimal.CurrentColorMode = ColorMode.Color;
            minimal.CurrentResolution = scanner.SupportedResolutions[0];
            minimal.CurrentSource = ScanSource.Flatbed;
            return Acquire(minimal);
        }

        public string Name
        {
            get 
            {
                return "TWAIN";
            }
        }

        class ScanningSession
        {
            internal TwUserInterface ui;
            internal TwainSession currentApp;
            internal bool moreImagesToScan;
        }

        ScanningSession session;

        public object BeginAcquire(ScannerInfo scanner)
        {
            if (session != null)
                throw new Exception("Scanning session already active");
            session = new ScanningSession();
            OpenDSM();
            CurrentApp.CurrentDS = GetDataSource(scanner.DeviceID);
            if (!OpenDS())
                throw new Exception("Open datasource failed.");
            SetupScanner(scanner);
            // enable data source
            session.ui = EnableDataSource();
            session.currentApp = CurrentApp;
            session.moreImagesToScan = true; // suppose we do have images to scan
            return session;
        }

        /// <summary>
        /// actually, we're not using session object since just one session is possible currently
        /// </summary>
        /// <param name="session"></param>
        public void EndAcquire(object ssession)
        {            
            if (session == null)
                throw new Exception("Scanning session must be started with BeginAcquire()");
            DisableDataSource();
            session = null;
            CloseDS();
            CloseDSM();
        }

        private void DisableDataSource()
        {
            AssertOK(Twain.DSuserif(CurrentApp.AppId, CurrentApp.CurrentDS, TwDG.Control, TwDAT.UserInterface, TwMSG.DisableDS, session.ui),
                "Cannot disable datasource");
            CurrentApp.Handler.EndListen();
        }

        #endregion
    }
}
